package repository

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"mall/common/globalkey"
	"mall/common/modelx"
	"mall/service/user/model"

	"github.com/volatiletech/null/v8"
	"github.com/volatiletech/sqlboiler/v4/boil"
	"github.com/volatiletech/sqlboiler/v4/queries/qm"
	"github.com/zeromicro/go-zero/core/stores/cache"
)

type (
	// UserAddressRepository is an interface to be customized, add more methods here,
	// and implement the added methods in customUserAddressRepository.
	UserAddressRepository interface {
		FindOne(ctx context.Context, id uint64) (*model.UserAddress, error)
		FindOneWithExpire(
			ctx context.Context,
			id uint64,
			expire time.Duration,
		) (*model.UserAddress, error)
		FindOneNoCache(
			ctx context.Context,
			conn boil.ContextExecutor,
			id uint64,
		) (*model.UserAddress, error)
		GetList(
			ctx context.Context,
			cols model.M,
			withCount int,
			filter map[string]map[string][]any,
		) ([]*model.UserAddress, int64, error) // ##25.05.13##
		Insert(
			ctx context.Context,
			tx boil.ContextExecutor,
			data *model.UserAddress,
		) error
		Update(
			ctx context.Context,
			tx boil.ContextExecutor,
			data *model.UserAddress,
		) error
		UpdateBatch(ctx context.Context, tx boil.ContextExecutor, ids []uint64, cols model.M) error
		SoftDelete(ctx context.Context, tx boil.ContextExecutor, id uint64, userId uint64) error
		Restore(ctx context.Context, tx boil.ContextExecutor, id uint64) error
		ForceDelete(ctx context.Context, tx boil.ContextExecutor, id uint64) error
		ForceDeleteBatch(ctx context.Context, tx boil.ContextExecutor, ids []uint64) error
	}

	customUserAddressRepository struct {
		modelx.CachedConn
		mysqlConn           *sql.DB
		cacheIdPrefix       string
		cacheIdExpirePrefix string
	}
)

// NewUserAddressRepository returns a repository for the database table.
func NewUserAddressRepository(mysqlConn *sql.DB, cacheConf cache.CacheConf) UserAddressRepository {
	return &customUserAddressRepository{
		CachedConn:          modelx.NewConn(mysqlConn, cacheConf),
		mysqlConn:           mysqlConn,
		cacheIdPrefix:       "cache:user:user_addresses:id:",
		cacheIdExpirePrefix: "cache:user:user_addresses:id:expire:",
	}
}

func (r *customUserAddressRepository) FindOne(
	ctx context.Context,
	id uint64,
) (*model.UserAddress, error) {
	cacheIdKey := r.formatPrimary(id)
	var resp model.UserAddress
	err := r.QueryCtx(ctx, &resp, cacheIdKey, func(ctx context.Context, conn *sql.DB, v any) error {
		data, err := model.FindUserAddress(ctx, conn, id)
		if err != nil {
			return err
		}

		*v.(*model.UserAddress) = *data

		return nil
	})

	switch err {
	case nil:
		return &resp, nil
	case sql.ErrNoRows:
		return nil, model.ErrNotFound
	default:
		return nil, err
	}
}

func (r *customUserAddressRepository) FindOneWithExpire(
	ctx context.Context,
	id uint64,
	expire time.Duration,
) (*model.UserAddress, error) {
	cacheIdKey := r.formatPrimaryWithExpire(id)
	var resp model.UserAddress
	err := r.QueryWithExpireCtx(ctx, &resp, cacheIdKey, expire, func(ctx context.Context, conn *sql.DB, v any) error {
		data, err := model.FindUserAddress(ctx, conn, id)
		if err != nil {
			return err
		}

		*v.(*model.UserAddress) = *data

		return nil
	})

	switch err {
	case nil:
		return &resp, nil
	case sql.ErrNoRows:
		return nil, model.ErrNotFound
	default:
		return nil, err
	}
}

func (r *customUserAddressRepository) FindOneNoCache(
	ctx context.Context,
	conn boil.ContextExecutor,
	id uint64,
) (*model.UserAddress, error) {
	var db boil.ContextExecutor = r.mysqlConn
	if conn != nil {
		db = conn
	}
	data, err := model.FindUserAddress(ctx, db, id)
	switch err {
	case nil:
		return data, nil
	case sql.ErrNoRows:
		return nil, model.ErrNotFound
	default:
		return nil, err
	}
}

// withCount
// -1-返回list；0-返回count；1-返回count和list；2-根据offset返回count和list；
// 3-根据preMinId返回count和list；4-根据preMaxId返回count和list；5-统计参与次数、统计领取次数
func (r *customUserAddressRepository) GetList(
	ctx context.Context,
	cols model.M,
	withCount int,
	filter map[string]map[string][]any,
) (list []*model.UserAddress, count int64, err error) {
	var page int
	var pageSize int
	var limit int
	var selectCol string
	var mods []qm.QueryMod
	var nextCount int64
	var previousCount int64
	for k, v := range cols {
		switch k {
		case "page":
			if val, ok := v.(uint64); ok && val > 1 {
				page = int(val)
			}
		case "pageSize":
			if val, ok := v.(uint64); ok && val > 0 {
				pageSize = int(val)
			}
		case "limit":
			if val, ok := v.(uint32); ok && val > 0 {
				limit = int(val)
			}
		case "offset":
			if withCount == 2 {
				if val, ok := v.(uint32); ok && val > 0 {
					mods = append(mods, qm.Offset(int(val)))
					previousCount = 1
				}
			}
		case "orderBy":
			if val, ok := v.(string); ok && len(val) > 0 {
				mods = append(mods, qm.OrderBy(val))
			}
		case "preMinId":
			if withCount == 3 {
				if val, ok := v.(uint64); ok && val > 0 {
					mods = append(mods, qm.OrderBy(model.UserAddressColumns.UserAddressID+" DESC"))
					mods = append(mods, model.UserAddressWhere.UserAddressID.LT(val))
					previousCount = 1
				}
			}
		case "preMaxId":
			if withCount == 4 {
				if val, ok := v.(uint64); ok && val > 0 {
					mods = append(mods, qm.OrderBy(model.UserAddressColumns.UserAddressID+" ASC"))
					mods = append(mods, model.UserAddressWhere.UserAddressID.GT(val))
					previousCount = 1
				}
			}
		case "select":
			if withCount != 5 {
				if val, ok := v.(string); ok && len(val) > 0 {
					selectCol = val
				}
			}
		}
	}

	for key, opMap := range filter {
		switch key {
		case model.UserAddressColumns.UserAddressID:
			mods = append(mods, modelx.GetIntMods(model.UserAddressColumns.UserAddressID, opMap)...)
		case model.UserAddressColumns.Label:
			mods = append(mods, modelx.GetStringMods(model.UserAddressColumns.Label, opMap)...)
		case model.UserAddressColumns.UserID:
			mods = append(mods, modelx.GetIntMods(model.UserAddressColumns.UserID, opMap)...)
		case model.UserAddressColumns.Consignee:
			mods = append(mods, modelx.GetStringMods(model.UserAddressColumns.Consignee, opMap)...)
		case model.UserAddressColumns.Mobile:
			mods = append(mods, modelx.GetStringMods(model.UserAddressColumns.Mobile, opMap)...)
		case model.UserAddressColumns.Province:
			mods = append(mods, modelx.GetStringMods(model.UserAddressColumns.Province, opMap)...)
		case model.UserAddressColumns.City:
			mods = append(mods, modelx.GetStringMods(model.UserAddressColumns.City, opMap)...)
		case model.UserAddressColumns.District:
			mods = append(mods, modelx.GetStringMods(model.UserAddressColumns.District, opMap)...)
		case model.UserAddressColumns.Street:
			mods = append(mods, modelx.GetStringMods(model.UserAddressColumns.Street, opMap)...)
		case model.UserAddressColumns.DetailedAddress:
			mods = append(mods, modelx.GetStringMods(model.UserAddressColumns.DetailedAddress, opMap)...)
		case model.UserAddressColumns.PostalCode:
			mods = append(mods, modelx.GetStringMods(model.UserAddressColumns.PostalCode, opMap)...)
		case model.UserAddressColumns.RegionCode:
			mods = append(mods, modelx.GetStringMods(model.UserAddressColumns.RegionCode, opMap)...)
		case model.UserAddressColumns.IsDefault:
			mods = append(mods, modelx.GetBooleanMods(model.UserAddressColumns.IsDefault, opMap)...)
		case model.UserAddressColumns.Status:
			mods = append(mods, modelx.GetIntMods(model.UserAddressColumns.Status, opMap)...)
		case model.UserAddressColumns.LastUsedAt:
			mods = append(mods, modelx.GetTimeMods(model.UserAddressColumns.LastUsedAt, opMap)...)
		case model.UserAddressColumns.CreatedAt:
			mods = append(mods, modelx.GetTimeMods(model.UserAddressColumns.CreatedAt, opMap)...)
		case model.UserAddressColumns.UpdatedAt:
			mods = append(mods, modelx.GetTimeMods(model.UserAddressColumns.UpdatedAt, opMap)...)
		case model.UserAddressColumns.DeletedAt:
			mods = append(mods, modelx.GetTimeMods(model.UserAddressColumns.DeletedAt, opMap)...)
		}
	}

	if withCount >= 2 {
		if limit > 0 {
			mods = append(mods, qm.Limit(limit+1))
		}
	} else if withCount >= 0 {
		count, err = model.UserAddresses(mods...).Count(ctx, r.mysqlConn)
		if err != nil {
			return
		}

		if withCount == 0 {
			return
		}
	}

	if pageSize > 0 && (withCount == -1 || withCount == 1) {
		if page > 1 {
			mods = append(mods, qm.Offset(pageSize*(page-1)))
		}
		mods = append(mods, qm.Limit(pageSize))
	}

	var columns []string
	if len(selectCol) > 0 {
		if selectCol == "," {
			columns = append(columns, model.UserAddressColumns.UserAddressID)
		} else {
			columns = strings.Split(selectCol, ",")
		}
	}
	if len(columns) > 0 {
		mods = append(mods, qm.Select(columns...))
	}

	list, err = model.UserAddresses(mods...).All(ctx, r.mysqlConn)
	if err != nil {
		return
	}

	if withCount >= 2 {
		if limit > 0 && len(list) > limit {
			nextCount = 1
			list = list[:limit]
		}
		count = previousCount + nextCount*2
	}

	return
}

func (r *customUserAddressRepository) Insert(
	ctx context.Context,
	tx boil.ContextExecutor,
	data *model.UserAddress,
) error {
	err := r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
		var db boil.ContextExecutor
		if tx != nil {
			db = tx
		} else {
			db = conn
		}

		err := data.Insert(ctx, db, boil.Infer())
		if err != nil {
			return err
		}

		return nil
	}, r.getCacheKeys(data)...)

	return err
}

func (r *customUserAddressRepository) Update(
	ctx context.Context,
	tx boil.ContextExecutor,
	data *model.UserAddress,
) error {
	err := r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
		var db boil.ContextExecutor
		if tx != nil {
			db = tx
		} else {
			db = conn
		}

		if data != nil {
			rowsAff, err := data.Update(ctx, db, boil.Blacklist(
				model.UserAddressColumns.UserAddressID,
				model.UserAddressColumns.DeletedAt,
			))
			if err != nil {
				return err
			}

			if rowsAff == 0 {
				return model.ErrNotUpdated
			}
		}

		return nil
	}, r.getCacheKeys(data)...)

	return err
}

func (r *customUserAddressRepository) UpdateBatch(
	ctx context.Context,
	tx boil.ContextExecutor,
	ids []uint64,
	cols model.M,
) error {
	if len(ids) > 0 {
		err := r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
			var db boil.ContextExecutor
			if tx != nil {
				db = tx
			} else {
				db = conn
			}

			var mods []qm.QueryMod
			mods = append(mods, model.UserAddressWhere.UserAddressID.IN(ids))
			rowsAff, err := model.UserAddresses(mods...).UpdateAll(ctx, db, cols)
			if err != nil {
				return err
			}

			if rowsAff == 0 {
				return model.ErrNotUpdated
			}

			return nil
		}, r.formatPrimaries(ids)...)

		return err
	}

	return nil
}

func (r *customUserAddressRepository) SoftDelete(
	ctx context.Context,
	tx boil.ContextExecutor,
	id uint64,
	userId uint64,
) error {
	data, err := r.FindOneNoCache(ctx, tx, id)
	if err != nil {
		if err == model.ErrNotFound {
			return nil
		}

		return err
	}

	if data.UserID != userId {
		return nil
	}

	if data.Status == uint8(globalkey.StatusDeleted) {
		return nil
	}

	data.DelStatus = data.Status
	data.Status = uint8(globalkey.StatusDeleted)
	data.DeletedAt = null.TimeFrom(time.Now())

	err = r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
		var db boil.ContextExecutor
		if tx != nil {
			db = tx
		} else {
			db = conn
		}

		rowsAff, err := data.Update(ctx, db, boil.Blacklist(
			model.UserAddressColumns.UserAddressID,
			model.UserAddressColumns.UpdatedAt,
		))
		if err != nil {
			return err
		}

		if rowsAff == 0 {
			return model.ErrNotUpdated
		}

		return nil
	}, r.getCacheKeys(data)...)

	return err
}

func (r *customUserAddressRepository) Restore(
	ctx context.Context,
	tx boil.ContextExecutor,
	id uint64,
) error {
	data, err := r.FindOneNoCache(ctx, tx, id)
	if err != nil {
		if err == model.ErrNotFound {
			return nil
		}

		return err
	}

	if data.Status != uint8(globalkey.StatusDeleted) {
		return nil
	}

	data.Status = data.DelStatus
	data.DeletedAt = null.TimeFromPtr(nil)

	err = r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
		var db boil.ContextExecutor
		if tx != nil {
			db = tx
		} else {
			db = conn
		}

		rowsAff, err := data.Update(ctx, db, boil.Blacklist(
			model.UserAddressColumns.UserAddressID,
			model.UserAddressColumns.UpdatedAt,
		))
		if err != nil {
			return err
		}

		if rowsAff == 0 {
			return model.ErrNotUpdated
		}

		return nil
	}, r.getCacheKeys(data)...)

	return err
}

func (r *customUserAddressRepository) ForceDelete(
	ctx context.Context,
	tx boil.ContextExecutor,
	id uint64,
) error {
	data, err := r.FindOneNoCache(ctx, tx, id)
	if err != nil {
		if err == model.ErrNotFound {
			return nil
		}

		return err
	}

	err = r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
		var db boil.ContextExecutor
		if tx != nil {
			db = tx
		} else {
			db = conn
		}

		rowsAff, err := data.Delete(ctx, db)
		if err != nil {
			return err
		}

		if rowsAff == 0 {
			return model.ErrNotDeleted
		}

		return nil
	}, r.getCacheKeys(data)...)

	return err
}

func (r *customUserAddressRepository) ForceDeleteBatch(
	ctx context.Context,
	tx boil.ContextExecutor,
	ids []uint64,
) error {
	if len(ids) > 0 {
		err := r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
			var db boil.ContextExecutor
			if tx != nil {
				db = tx
			} else {
				db = conn
			}

			var mods []qm.QueryMod
			mods = append(mods, model.UserAddressWhere.UserAddressID.IN(ids))

			rowsAff, err := model.UserAddresses(mods...).DeleteAll(ctx, db)
			if err != nil {
				return err
			}

			if rowsAff == 0 {
				return model.ErrNotDeleted
			}

			return nil
		}, r.formatPrimaries(ids)...)

		return err
	}

	return nil
}

func (r *customUserAddressRepository) getCacheKeys(data *model.UserAddress) []string {
	if data == nil {
		return []string{}
	}

	cacheIdKey := r.formatPrimary(data.UserAddressID)
	cacheIdExpireKey := r.formatPrimaryWithExpire(data.UserAddressID)
	cacheKeys := []string{
		cacheIdKey,
		cacheIdExpireKey,
	}

	return cacheKeys
}

func (r *customUserAddressRepository) formatPrimary(primary any) string {
	return fmt.Sprintf("%s%v", r.cacheIdPrefix, primary)
}

func (r *customUserAddressRepository) formatPrimaryWithExpire(primary any) string {
	return fmt.Sprintf("%s%v", r.cacheIdExpirePrefix, primary)
}

func (r *customUserAddressRepository) formatPrimaries(primaries []uint64) []string {
	var resp []string
	for _, primary := range primaries {
		resp = append(resp, r.formatPrimary(primary))
		resp = append(resp, r.formatPrimaryWithExpire(primary))
	}

	return resp
}

func (r *customUserAddressRepository) queryPrimary(ctx context.Context, conn *sql.DB, v any, primary any) error {
	var uid uint64
	if jsonId, ok := primary.(json.Number); ok {
		if int64Id, err := jsonId.Int64(); err == nil {
			uid = uint64(int64Id)
		} else {
			return err
		}
	}

	data, err := model.FindUserAddress(ctx, conn, uid)
	if err != nil {
		return err
	}

	*v.(*model.UserAddress) = *data

	return nil
}
